(ns crypt-of-calamity.core
  (:import (java.awt Color Dimension Graphics)
           (javax.swing JPanel JFrame Timer JOptionPane)
           (java.awt.event ActionListener KeyListener MouseListener)
           (com.wapmx.nativeutils.jniloader NativeLoader DefaultJniExtractor)
           (java.io File FileOutputStream InputStream OutputStream))
  (:use clojure.contrib.import-static
        clojure.set
        [clojure.contrib.seq-utils :only (includes?)]))

(import-static java.awt.event.KeyEvent VK_LEFT VK_RIGHT VK_UP VK_DOWN)

(def rnd (new java.util.Random (. java.lang.System currentTimeMillis)))

(defn and-reduce [pred list]
  (reduce (fn [a b] (and a b)) true (map pred list)))

;debugging parts of expressions
(defmacro dbg [x]
  `(let [x# ~x]
    (println "dbg:" '~x "=" x#)
    x#))

(def dir-set #{ :up :down :left :right })

(def dirs { :up    [ 0  1]
            :down  [ 0 -1]
            :left  [-1  0]
            :right [ 1  0]})

(def opposite-dirs { :up    :down
                     :down  :up
                     :left  :right
                     :right :left } )

(def perp-dirs { :up    #{ :left :right }
                 :down  #{ :left :right }
                 :left  #{ :up   :down  }
                 :right #{ :up   :down  } })


(def dir-keys { VK_UP     :up
               VK_DOWN   :down
               VK_LEFT   :left
               VK_RIGHT  :right })

(def classes '(:wizard
               :alchemist
               :baker
               :jewler
               :builder
               :warrior
               :scribe
               :gypsy
               :barrister
               :minstrel
               :jones))

(def stats '(:strength
             :stamina
             :wisdom
             :intelligence
             :dexterity
             :charisma
             :luck))

; an object in the dungeon (item, monster, wall, door...)
; type - keyword
(defstruct thing :type)

; coords - point - a two element [x y] vector
; things - list - a list of things in the location
(defstruct location :coords :things)

; name - string - the character's name
; class - keyword - from the classes list
; location - the character's location
; stats - map - a map of the form generated by roll-stats
(defstruct character :name :class :location :stats)

(defn roll-stat
  "a random number generator for creating stat values"
  ([max] (+ 1 (. rnd nextInt max)))
  ([] (roll-stat 20)))


(defn roll-stats
	"returns a map containing each stat and a randomly generated value for that stat"
	[class]
  	(apply hash-map
    	(interleave stats
      	(take (count stats)
        	(repeatedly roll-stat)))))

(defn add-points [& pts]
  (vec (apply map + pts)))

(defn create-character [name class]
  (struct-map character :name name
                        :class class 
                        :location [0 0] 
                        :stats (roll-stats class)))






(defn element-list
	"helper function for cross-join example: (element-list 1 '(1 2 3)) -> ((1 1) (1 2) (1 3))"
	[n l]
 	 (map #(list n %) l))

(defn list-list
[l1 l2]
  (apply concat (map #(element-list % l2) l1)))

(defn cross-join [& lists]
  (map flatten (reduce list-list lists)))

(defn create-locations
  ([width height] (create-locations [0 0] width height))
  ([[x y] width height]
    (map #(struct-map location :coords % :things #{})
          (cross-join (range x (+ width x))
                      (range y (+ height y))))))

(defn create-dungeon [width height]
  (let [locations-list (create-locations width height)]
    (apply hash-map (interleave (map :coords locations-list)  locations-list))))

(defn add-location [dungeon location]
  (assoc dungeon (:coords location) location))

(defn add-coords [dungeon coords]
  (assoc dungeon coords (struct-map location :coords coords :things #{})))

(defn add-thing [dungeon coords thing]
  (let [location (dungeon coords)
        things (:things location)
        new-things (conj things thing)
        new-location (assoc location :things new-things)]
    (assoc dungeon coords new-location)))

(defn remove-thing [dungeon coords thing]
  (let [location (dungeon coords)
        things (:things location)
        new-things (disj things thing)
        new-location (assoc location :things new-things)]
    (assoc dungeon coords new-location)))

(defn fill-with-walls [dungeon]
  (loop [coords (keys dungeon)
         new-dungeon dungeon]
    (if coords
      (let [coord (first coords)
            location (new-dungeon coords)]
        (recur (next coords) (add-thing new-dungeon coord :wall)))
      new-dungeon)))

; test whether a location is a wall
; i.e. whether its :things set contains a :wall
(defn wall? [{:keys [things]}] (:wall things))

; tests whether a location is at the given coords
(defn at-coords? [query-coords {:keys [coords]}]
  (if (nil? coords)
    nil
    (reduce (fn [a b] (and a b)) true (map = query-coords coords))))

; get the location for a given set of coordinates in the dungeon
(defn get-location [dungeon coords]
  (dungeon coords))

; test whether a given set of coordinates in the dungeon exists
(defn has-location?
  ([dungeon coords] (dungeon coords)))

(defn has-thing?
  ([dungeon coords thing] (has-thing? (get-location dungeon coords) thing))
  ([location thing] (contains? (:things location) thing)))


(defn blocked? [dungeon coords]
  (let [location (get-location dungeon coords)]
    (or (nil? location) (has-thing? dungeon coords :wall))))

; return the set of directions which are blocked
(defn edges [dungeon coords]
  (let [new-coord (fn [dir] (add-points coords (dir dirs)))]
    (filter #(blocked? dungeon (new-coord %)) dir-set)))

; return the set of directions which are not blocked
(defn non-edges [dungeon coords]
  (difference dir-set (apply hash-set (edges dungeon coords))))

; return whether the location at the specified coordinates is adjacent to any blocked locations
(defn edge? [dungeon coords]
  (not (empty? (edges dungeon coords))))

; returns which of the four locations adjacent to the given coordinate meet the specified predicate
(defn filter-surrounding [dungeon coords predicate]
  (filter predicate
          (map get-location
               (repeat dungeon)
               (map add-points
                    (repeat coords)
                    (vals dirs)))))

; returns a random element from list
(defn get-random-element [list]
  (if (empty? list)
    nil
    (last (take (+ (. rnd nextInt (count list)) 1) list))))

; returns a random wall location from a dungeon
(defn rand-wall [dungeon]
  (get-random-element (filter wall? (vals dungeon))))


; a predicate for use with tunnel
(defn exists-and-blocked?
  ([dungeon current-coord new-coord direction] (exists-and-blocked? dungeon new-coord))
  ([dungeon coord] (and (has-location? dungeon coord) (blocked? dungeon coord))))

(defn and-reduce [pred list]
  (reduce (fn [a b] (and a b)) true (map pred list)))

(defn wall-buffer? [dungeon current-coord new-coord direction]
  (let [other-dirs (difference dir-set #{(opposite-dirs direction)})]
    (and-reduce (fn [dir] (exists-and-blocked? dungeon (add-points (dirs dir) new-coord))) other-dirs)))


; steps through the dungeon, starting at current-coord
; locations will only be visited if
; (predicate dungeon current-coord new-coord direction) is true
(defn tunnel [dungeon current-coord pred]
  (loop [new-dungeon (remove-thing dungeon current-coord :wall)
         directions dir-set]
    (let [direction (get-random-element directions)
          new-directions (difference directions #{direction})
          new-coord (add-points (dirs direction) current-coord)]
      (if direction
        (if (pred new-dungeon current-coord new-coord direction)
          (recur (tunnel new-dungeon new-coord pred) new-directions)
          (recur new-dungeon new-directions))
        new-dungeon))))


(defn print-dungeon [dungeon]
  (let [coords-x (map #(first (:coords %)) (vals dungeon))
        coords-y (map #(second (:coords %)) (vals dungeon))
        min-x (apply min coords-x)
        max-x (apply max coords-x)
        min-y (apply min coords-y)
        max-y (apply max coords-y)]
    (loop [y max-y]
      (if (<= 0 y)
        (do
          (println (map #(if (blocked? dungeon [% y]) 1 0) (range min-x (inc max-x))))
          (recur (dec y)))))))





(defn render [ #^Graphics g w h ]
	(doto g
		(.setColor (Color/black))
		(.fillRect 0 0 w h)))

(defn create-dungeon-panel []
	"Create a panel which renders a dungeon object"
	(proxy [JPanel] []
		(paintComponent [g]
			(proxy-super paintComponent g)
			(render g (. this getWidth) (. this getHeight)))))

(defn copy [^InputStream in ^OutputStream out]
  (let [tmp (byte-array 8192)]
    (loop []
      (let [new-len (. in read tmp)]
        (if (> new-len 0)
          (do (. out write tmp 0 new-len)) (recur))))))

(defn extractResource [^InputStream in ^File out-file]
  (let [^OutputStream out (FileOutputStream. out-file)]
    (copy in out)
    (. in close)
    (. out close)
    out-file))

(def jni-extractor
  (proxy [DefaultJniExtractor] []
    (extractJni [lib-name]
      (let [mapped-lib (System/mapLibraryName lib-name)
            ^InputStream in (.. this (getClass) (getClassLoader) (getResourceAsStream mapped-lib))
            ^File out-file  (File. (. this (getJniDir)) mapped-lib)]
        (extractResource in out-file)))))

(defn crypt-of-calamity []
	(let [frame (JFrame. "Crypt of Calamity")
				dungeon-panel (create-dungeon-panel)]
    (NativeLoader/setJniExtractor jni-extractor)
		(NativeLoader/loadLibrary "jogl")
    (.addMouseListener dungeon-panel
			(proxy [MouseListener] []
				(mouseClicked [event]
					(println (str "clicked")))
				(mouseEntered [event])
				(mouseExited [event])
				(mousePressed [event])
				(mouseReleased [event])))
		(doto frame
			(.add dungeon-panel)
			(.setSize 800 600)
			(.setVisible true))))

